package com.espirit.eap.manager;

import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import javax.annotation.Resource;

import org.apache.commons.lang.ObjectUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.log4j.Logger;
import org.apache.struts2.ServletActionContext;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import com.espirit.eap.pagelayout.AvabButton;
import com.espirit.eap.pagelayout.Model;
import com.espirit.eap.pagelayout.ModelService;
import com.espirit.eap.pagelayout.Page;
import com.espirit.eap.pagelayout.ResourceManagerService;
import com.espirit.eap.sa.LoginUser;
import com.espirit.eap.sa.LoginUserService;
import com.espirit.eap.sa.PrivilegeService;
import com.espirit.eap.util.ChineseSpeller;
import com.espirit.eap.util.Functions;
import com.googlecode.cswish.annotation.ParamScope;
import com.googlecode.cswish.annotation.Rest;
import com.googlecode.cswish.exception.ApplicationException;
import com.googlecode.cswish.model.PageElements;
import com.googlecode.cswish.model.PageInfo;
import com.googlecode.cswish.model.ParameterMap;
import com.googlecode.cswish.model.Reference;
import com.googlecode.cswish.oosearch.Condition;
import com.googlecode.cswish.oosearch.QLInfo;
import com.googlecode.cswish.security.SafeManager;
import com.googlecode.cswish.struts.ContextHolder;
import com.googlecode.cswish.struts.Invoker;
import com.googlecode.cswish.struts.Routing;
import com.googlecode.cswish.struts.spring.BeanIntrospector;
import com.googlecode.cswish.struts.spring.GenericService;
import com.googlecode.cswish.util.FrameConstant;

@Service
public class MenuService {
	
	private final static Logger logger = Logger.getLogger(MenuService.class);
	
	@Resource
	private GenericService genericService;
	
	@Resource
	private BeanIntrospector beanIntrospector;
	
	@Resource
	private Functions functions;
	
	@Resource
	private SafeManager safeManager;
	
	@Resource
	private ModelService modelService;
	
	@Resource
	private I18nService i18nService;
	
	@Resource
	private Routing routing;

	@Resource
	private PrivilegeService privilegeService;
	
	@Resource
	private ResourceManagerService resourceManagerService;
	
	@Resource
	private LoginUserService loginUserService;
	
	private Set<String> skippedMethods;
	
	// TODO: temporary solution, remove it
	private Set<String> sharedMenus;
	
	public MenuService() {
		// add , delete, deleteList, update use generic template
		skippedMethods = new HashSet<String>();
		skippedMethods.add("add");
		skippedMethods.add("delete");
		skippedMethods.add("deleteList");
		skippedMethods.add("update");
		// search method is same with "_search"
		skippedMethods.add("search");
		
		// NOTICE: website menu only belongs to the cswish
		sharedMenus = new HashSet<String>();
		sharedMenus.add("/espirit/eap/manager/workflow/wFAutoNode.search.html");
		sharedMenus.add("/espirit/eap/pagelayout/page.search.html");
		sharedMenus.add("/espirit/eap/pagelayout/model.search.html");
		sharedMenus.add("/espirit/eap/pagelayout/pagePlugin.search.html");
		sharedMenus.add("/espirit/eap/sa/validation.search.html");
		sharedMenus.add("/espirit/eap/sa/loginUser.search.html");
		sharedMenus.add("/espirit/eap/manager/dbBackupHistory._search.html");
		sharedMenus.add("/espirit/eap/manager/i18n.search.html");
		sharedMenus.add("/espirit/eap/manager/menu.search.html");
	}
	
	@SuppressWarnings("unchecked")
	@Rest(type="search")
	public PageInfo<Menu> searchParentMenu(int start, int limit) {
		String ql = " from " + Menu.class.getName() + " a where a.page is null";
		
		// group by menu
		QLInfo qlInfo = new QLInfo(ql, true);
		int count = genericService.getCount(qlInfo, false);
		PageInfo<Menu> pageInfo = genericService.searchByQl(qlInfo, true);
		List<Menu> data = new ArrayList<Menu>(pageInfo.getData().size() + 1);
		data.add(new Menu());
		// first level
		for (Menu menu : pageInfo.getData()) {
			if (menu.getParent() == null) {
				data.add(menu);
			}
		}
		// second level & third level
		for (Menu menu : pageInfo.getData()) {
			if (menu.getParent() != null) {
				
				String i18n = menu.getI18n();
				if (i18n != null) {
					String newI18n = "|----" + i18nService.getI18n(i18n).getName();
					menu.setI18n(newI18n);
				}
				
				// find the parent
				Integer parentId = menu.getParent().getId();
				int i = getParentIndex(data, parentId);
				if (i == data.size()) {		//no found, parent donesn't add
					// find again
					parentId = menu.getParent().getParent().getId();
					i = getParentIndex(data, parentId);
				}
				data.add(i + 1, menu);
			}
		}
		
		pageInfo.setData(data);
		pageInfo.setTotalCount(count);
		return pageInfo;
	}
	
	private int getParentIndex(List<Menu> data, Integer parentId) {
		int i = 0, len = data.size();
		for (; i < len; i++) {
			Menu item = data.get(i);
			if (parentId.equals(item.getId())) {
				break;
			}
		}
		return i;
	}
	
	// TODO: better way to filter the site menu?
	public PageInfo<Menu> search(ParameterMap model,
    		int start, int limit, String sort, String dir, boolean newSearch,
    		Reference<QLInfo> qlInfoRef, @ParamScope(visible=false, value="#ps")PageElements ps) {
		PageInfo pageInfo = genericService.search(model, start, limit, sort, dir, newSearch, qlInfoRef);
		return pageInfo;
	}
	
	public Menu createMenu(String key,String enName,String cnName,Menu parentMenu, String site){
		Menu menu = new Menu();
		menu.setName(key);
		i18nService.addI18n(cnName, cnName, enName);
		menu.setI18n(cnName);
		return createMenu(menu, parentMenu, site);
	}
	public Menu createMenu(Class clazz,String enName,String cnName,Menu parentMenu, String site){
		String simpleName = beanIntrospector.getSimpleName(clazz.getName());
		Menu menu = new Menu();
		menu.setName(simpleName);
		return createMenu(clazz, simpleName, parentMenu, null, site);
	}
	
	public Menu createMenu(Class clazz,Menu parentMenu, String site){
		return createMenu(clazz, "search", parentMenu, site);
	}
	
	public Menu createMenu(Class clazz, String method, Menu parentMenu, String site){
		Model model = modelService.getModel(clazz);
		if (model == null) return null;
		String i18n = model.getI18n();
		return createMenu(clazz, i18n, parentMenu, method, site);
	}


	private Page getMenuPage(String simpleName , String method){
		Page page = new Page();
		page.setPath("/"+simpleName.replace(".", "/") +  "." + method + "." + functions.getActionExtension());
		Page existed = genericService.searchAll(page).getData(0);
		if (existed != null) {
			page = existed;
		}
		page.setFreeAccess(false);
		page.setTemplate(false);
		page.setMethod(method);
		if (page.getCommonTemplate() == null){
			page.setCommonTemplate(false);
		}
		return page;

	}
	private Menu createMenu(Class clazz,String i18n,Menu parentMenu,String method, String site){
		String simpleName = beanIntrospector.getSimpleName(clazz.getName());
		Menu menu = new Menu();
		Model model = modelService.getModel(clazz);
		if (model == null) return null;
		menu.setI18n(i18n);
		if (method == null){
			method = "search";
		}
		Page page = getMenuPage(simpleName, method);
		if (page.getI18n() == null) {
			I18n methodI18n = i18nService.getI18n("commons." + method, false);
			String name = i18n + ':' + (methodI18n == null ? method : methodI18n.getName());
			page.setI18n(name);
			if (page.getId() == null) {
				page = genericService.add(page);
			} else {
				page = genericService.update(page);
			}
		}
		menu.setPage(page);
		if (parentMenu != null){
			simpleName = parentMenu.getName() + '.' + simpleName;
		}
		menu.setName(simpleName);
		menu = createMenu(menu, parentMenu, site);
		
		return menu;
	}
	
	private boolean isValidMethod(String method) {
		return !skippedMethods.contains(method);
	}
	
	private Menu createMenu(Menu menu, Menu parentMenu, String site){
		Menu model = new Menu();
		model.setName(menu.getName());
		Menu existed = genericService.searchByModel(model , Condition.SEARCH_ONE).getData(0);
		if (existed != null) {
			Page page = existed.getPage();
			if (page == null && parentMenu != null){
				existed.setPage(menu.getPage());
				genericService.update(existed);
			}
			return existed;
		}
		
		Menu theMenu;
		if (parentMenu != null){
			menu.setParent(parentMenu);
			parentMenu.safeChildren().add(menu);
			menu.setItemNbr(parentMenu.safeChildren().size());
			
			genericService.update(parentMenu);
			theMenu = parentMenu.getChild(menu.getName());
		} else { 
			List<Menu> menus;
			String hql = "from " + Menu.class.getName() + " a  where a.parent is null";
			menus = genericService.searchByQl(new QLInfo(hql, false), true).getData();
			
			if (menus == null ){
				menu.setItemNbr(1);
			} else {
				menu.setItemNbr(menus.size() + 1);
			}
			theMenu = genericService.add(menu);
		}
		
		if (menu.getPage() != null) {
			Invoker invoker = routing.path2Invoker(menu.getPage().getPath(), false);
			String modelName = beanIntrospector.getModelName(beanIntrospector.getSimpleName(invoker.actionName));
			Model theModel = modelService.getModel(modelName);
			if (theModel != null) {				
				List<AvabButton> avabButtons = theModel.getAvabButtons();
				if (avabButtons != null) {
					List<Page> pages = new ArrayList<Page>();
					Set<String> addedButton = new HashSet<String>();
					for (int i = 0, len = avabButtons.size(); i < len; i++) {
						AvabButton avabButton = avabButtons.get(i);
						if (!isValidMethod(avabButton.getMethod()) || addedButton.contains(avabButton.getMethod())) {
							continue;
						}
						
						addedButton.add(avabButton.getMethod());
						String name;
						if (avabButton.getModel() != null) {
							name = avabButton.getModel();
						} else {
							name = modelName;
						}
						
						String method = avabButton.getMethod();
						String url;
						if (method == null || method.length() == 0) {
							url = avabButton.getExProperty("url");
						} else {
							url = functions.linkModelNoBase(name, method);
						}
						
						if (url != null) {
							Page page = new Page();
							page.setPath(url);
							Page existedPage = genericService.searchAll(page).getData(0);
							if (existedPage != null) {
								page = existedPage;
							}
							
							page.setMethod(avabButton.getMethod());
							
							String buttonI18n = avabButton.getI18n();
							if (buttonI18n == null) {
								buttonI18n = avabButton.getMethod();
							}
							page.setI18n(buttonI18n);
							
							page.setFreeAccess(false);
							page.setTemplate(false);
							page.setCommonTemplate(false);
							pages.add(page);
						}
					}
					theMenu.setRelevantPages(pages);
					genericService.update(theMenu);
				}
			}
		}
		
		return theMenu;
	}
		
	@Rest(simpleServiceName="loginUser", type="free")
	@Transactional
	public List<Menu> searchMenuList(int deep) {
		LoginUser loginUser = loginUserService.getLoginUser();
		if (loginUser == null) {
			return Collections.EMPTY_LIST;
		}
		
		// a.website.id is null means that the menu is shared by all user which has the permission
		String hql = "from " + Menu.class.getName() + " a  where a.parent is null order by a.itemNbr";
		PageInfo<Menu> lists = genericService.searchByQl(new QLInfo(hql, true), true);
		List<Menu> menus = new ArrayList<Menu>();
		String userId = safeManager.getLoginUser(ServletActionContext.getRequest()).getLoginId();
		
		// TODO: only show current site menu?
		String productName = ContextHolder.get().getProductName();
		for (Menu menu : lists.getData()) {
			Menu newMenu = updateMenuLink(userId, productName, menu);
			if (newMenu != null && !isBlankDir(newMenu)) {
				menus.add(newMenu);
			}
		}
		
		if (deep > 0) {
			List<Menu> refactorMenus = new ArrayList<Menu>();
			buildMenuList(menus, refactorMenus, deep);
			menus = refactorMenus;
		}
		return menus;
	}
	
	private void buildMenuList(List<Menu> menus, List<Menu> refactorMenus, int enableDeep) {
		for (Menu menu : menus) {
			List<Menu> childMenus = menu.getChildMenus();
			if (childMenus == null || childMenus.size() == 0) {
				// don't add the leaf menu
			} else {
				if (menu.getDeep() >= enableDeep) {		// use '>=' because the deep index begins from 0
					// skip this menu, its deep beyond the enabled deep, scan its children directly
					buildMenuList(childMenus, refactorMenus, enableDeep);
				} else {
					refactorMenus.add(menu);
				}
			}
		}
	}
	
	private boolean isBlankDir(Menu menu) {
		return !menu.isLeaf() && (menu.getChildMenus() == null || menu.getChildMenus().size() == 0);
	}
	
	private String getProductName(String path) {
		int index = path.indexOf('/', 1);
		return index >= 1 ? path.substring(1, index) : path;
	}
	
	private Menu updateMenuLink(String userId, String currentProductName, Menu menu) {
		if (menu.getPage() != null) {
			String path = menu.getPage().getPath();
			String productName = getProductName(path);
			boolean showCurrentProduct = false;
			if (showCurrentProduct && currentProductName != null && !currentProductName.equals(productName)
					&& !sharedMenus.contains(path)) {
				return null;
			}
			
			String pathWithSite = functions.linkNoBase(path, true);
			if (!safeManager.checkPage(userId, pathWithSite)) {
				return null;
			}
		}
		
		Menu newMenu = copyMenu(menu);
		List<Menu> children = menu.getChildMenus();
		if (children != null && children.size() > 0){
			List<Menu> newChildren = new ArrayList<Menu>(children.size());
			int maxDeep = 0;
			for (Menu child : children){
				Menu newChild = updateMenuLink(userId, currentProductName, child);
				if (newChild != null && !isBlankDir(newChild)) {
					newChildren.add(newChild);
					if (newChild.getDeep() > maxDeep) {
						maxDeep = newChild.getDeep();
					}
				}
			}
			newMenu.setChildren(newChildren);
			newMenu.setChildMenus(newChildren);
			newMenu.setDeep(maxDeep + 1);
		} else {
			// see createMenu(), it doesn't provide the path parameter, so add it here
			String path = menu.getPage() == null ? "" : menu.getPage().getPath();
			newMenu.setHref(functions.link(path, "newSearch=true&fromMenu=true", true));
			newMenu.setHrefTarget(menu.getName());
			//newMenu.setHrefTarget(category);
		}
		
		String i18n = newMenu.getI18n();
		if (i18n != null) {
			String text = i18nService.getI18n(i18n).getName();
			if (text == null) {
				newMenu.setText(i18n);
			} else {
				newMenu.setText(text);
			}
		} else {
			newMenu.setText(newMenu.getName());
		}
		
		return newMenu;
	}
	
	private Menu copyMenu(Menu menu) {
		Menu newMenu = new Menu();
		newMenu.setId(menu.getId());
		newMenu.setI18n(menu.getI18n());
		newMenu.setPage(menu.getPage());
		newMenu.setName(menu.getName());
		newMenu.setItemNbr(menu.getItemNbr());
		newMenu.setParent(menu.getParent());
		newMenu.setChildren(menu.getChildren(Menu.class));
		return newMenu;
	}
	
	public void _add(Menu model) {
		List<Page> pages = new ArrayList<Page>();
		// add all the common page
		String[] commonMethods = new String[] {"_add", "add", "_update", "update", "deleteList", "export", "_importFile"};
		String[] commonMethodI18nKeys = new String[] {"新增", "新增", "修改", "修改", "删除", "导出", "导入"};
		
		for (int index = 0; index < commonMethods.length; index++) {
			String method = commonMethods[index];
			Page page = new Page();
			page.setPath(method);
			I18n i18n = i18nService.getI18n(commonMethodI18nKeys[index]);
			page.setI18n(i18n.getCnName());
			pages.add(page);
		}
		Page page = new Page();
		page.setPath("search");
		page.setMethod("search");
		model.setPage(page);
		model.setRelevantPages(pages);
    }
	
	public Object _update(Menu model) {
		model = genericService.view(model);
		List<Page> pages = model.getRelevantPages();
		
		if (pages == null || pages.size() == 0) {
			pages = new ArrayList<Page>();
			pages.add(new Page());
			model.setRelevantPages(pages);
		}
		return model;
    }
	
	@Rest(type="add")
	public String getChineseSpell(String name) {
		return ChineseSpeller.convert(name, true, false);
	}

	private void updateMenuPages(Menu menu) {
		// update menu path
		if (menu.getParent() == null || menu.getParent().getId() == null 
				|| menu.getPage() == null || menu.getPage().getPath() == null || menu.getPage().getPath().length() == 0) {
			menu.setPage(null);
			menu.setRelevantPages(null);
		} else {
			updatePage(menu.getPage());
		}
		
		if (menu.getId() != null) {
			Menu oldMenu = genericService.load(Menu.class, menu.getId());
			removeFreePage(oldMenu);
		}
		if (menu.getPage() != null && (menu.getRelevantPages() != null && menu.getRelevantPages().size() > 0)) {
			// add/update it to privilege, TODO: remove the deleted privilege?
			List<Page> pages = menu.getRelevantPages();
			int len = pages.size();
			
			List<Page> noBlankPages = new ArrayList<Page>(len);
			for (int i = 0; i < len; i++) {
				Page page = pages.get(i);
				if (page == null) {
					continue;
				}
				
				boolean validPage = updatePage(page);
				if (validPage) {
					// reference
					noBlankPages.add(page);
				}
			}
			menu.setRelevantPages(noBlankPages);
		}
		
		updateFreePage(menu);
	}
	
	private boolean updatePage(Page page) {
		if (page.getTemplate() == null) {
			page.setTemplate(false);
		}
		String path = page.getPath();
		if (path == null || path.length() == 0) {
			return false;		// skip the invalid page
		}
		
		int pos1 = path.lastIndexOf('/') + 1;
		int pos2 = path.indexOf('.', pos1);
		String shortModelName = path.substring(pos1, pos2);
		if (shortModelName.endsWith(FrameConstant.SERVICE_PACKAGE_SUFFIX) || shortModelName.endsWith(FrameConstant.ACTION_PACKAGE_SUFFIX)) {
			throw new ApplicationException("errors.invalidPath", path);
		}
		
		// check path to avoid add duplicated page
		// ignore input id and try to merge by path (don't change the existed path to avoid the confused PageLayout)
		Page example = new Page();
		example.setPath(page.getPath());
		Page existed = genericService.searchAll(example).getData(0);
		if (existed != null) {
			page.setId(existed.getId());		// change id according the input path
		} else {
			// enable change the path to new path
		}
		
		Invoker invoker = routing.path2Invoker(path, false);
		page.setMethod(invoker.methodName);
		
		Date modifyOn = Calendar.getInstance().getTime();
		page.setModifyOn(modifyOn);
	
		// privilege
		if (page.getFreeAccess() == null) {
			page.setFreeAccess(Boolean.FALSE);
		}
		boolean freeAccess = page.getFreeAccess();
		if (!freeAccess) {
			privilegeService.addPrivilege(path);
		}
		return true;
	}
	
	private void removeFreePage(Menu menu) {
		if (menu == null) {
			return;
		}
		
		if (menu.getRelevantPages() != null) {
			for (Page page : menu.getRelevantPages()) {
				if (page.getFreeAccess() != null && page.getFreeAccess()) {
					String path = page.getPath();
					safeManager.removeFreePath(path);
					safeManager.removeFreePath(routing.getMatchPath(path));
				}
			}
		}
	}
	
	private void updateFreePage(Menu menu) {
		removeFreePage(menu);
		
		// functions.linkNoBase(actionPath, true)
		String sitePrefix = '/' + ContextHolder.get().getSiteName();
		
		// add the new free path
		if (menu.getPage() != null) {
			Page page = menu.getPage();
			String path = page.getPath();
			if (page.getFreeAccess() != null && page.getFreeAccess()) {
				safeManager.addFreePath(sitePrefix + path);
				safeManager.addFreePath(sitePrefix + routing.getMatchPath(path));
			}
		}
		
		if (menu.getRelevantPages() != null) {
			for (Page page : menu.getRelevantPages()) {
				if (page.getFreeAccess() != null && page.getFreeAccess()) {
					String path = page.getPath();
					safeManager.addFreePath(sitePrefix + path);
					safeManager.addFreePath(sitePrefix + routing.getMatchPath(path));
				}
			}
		}
	}
	
	
	/**
	 * NOTICE:
	 * <pre> 
	 * Menu = Model + Method
	 * menu i18n name is the Model name
	 * page i18n name is the method i18n name
	 * </pre>
	 * 
	 * @param model
	 * @return
	 */
	@Transactional
	public Menu add(Menu model, boolean enableMerge) {
		if (model.getId() != null) {
			return update(model);
		}
		
		// TODO: 
		if (model.getName() == null) {
			String path = null;
			if (model.getPage() != null) {
				path = model.getPage().getPath();
			}
		}
		String menuI18n = model.getI18n();
		if (menuI18n != null) {		// menu key must follows the key rule
			model.setI18n(menuI18n);
		}
		
		Menu example = new Menu();
		// example.setParent(model.getParent());
		example.setI18n(model.getI18n());			// TODO: correct? only use the resKey? or id?
		Menu dbMenu = genericService.searchByModel(example, Condition.SEARCH_ONE).getData(0);
		if (dbMenu != null) {
			if (enableMerge) {
				if (model.getId() == null) {
					model.setId(dbMenu.getId());
				}
				// merge operation, combine the children pages
				List<Page> combinePages = combineMenuPages(dbMenu.getRelevantPages(), model.getRelevantPages());
				model.setRelevantPages(combinePages);
				return update(model);
			} else {
				throw new ApplicationException("errors.duplicate", model.getI18n());
			}
		}
		
		updateMenuPages(model);
		
		// 5. add menu
		if (model.getParent() != null && model.getParent().getId() != null) {
			model = genericService.add(model);
			
			// update the parent/children relation ship
			Menu parentMenu = genericService.load(Menu.class, model.getParent().getId());
			parentMenu.safeChildren().add(model);
			model.setParent(parentMenu);
			if (model.getItemNbr() == null) {
				int maxNo = 0;
				for (Menu childMenu : parentMenu.getChildren()) {
					if (childMenu.getItemNbr() != null && childMenu.getItemNbr() > maxNo) {
						maxNo = childMenu.getItemNbr();
					}
				}
				model.setItemNbr(maxNo + 1);
			}
			genericService.merge(parentMenu);
			Menu matchedChild = null;
			for (Menu child : parentMenu.getChildMenus()) {
				if (StringUtils.equals(child.getName(), model.getName()) 
						&& ObjectUtils.equals(child.getItemNbr(), model.getItemNbr())) {
					matchedChild = child;
					break;
				}
			}
			return matchedChild;
		} else {
			if (model.getItemNbr() == null) {
				String hql = "from " + Menu.class.getName() + " a  where a.parent is null";
				int count = genericService.getCount(new QLInfo(hql, true), true);
				model.setItemNbr(count + 1);
			}
			return genericService.add(model);
		}
	}
	
	private List<Page> combineMenuPages(List<Page> dbPages, List<Page> modelPages) {
		List<Page> pages = new ArrayList<Page>();
		Collection<String> duplicateCheck = new HashSet<String>();
		if (dbPages != null) {
			pages.addAll(dbPages);
			for (Page page : dbPages) {
				String path = page.getPath();
				if (path != null) {
					duplicateCheck.add(path);
				}
			}
		}
		if (modelPages != null) {
			for (Page modelPage : modelPages) {
				if (modelPage.getPath() == null || !duplicateCheck.contains(modelPage.getPath())) {
					pages.add(modelPage);
				}
			}
		}
		return pages;
	}
	
	@Transactional
	public Menu update(Menu model) {
		// clean the cache
		Menu oldMenu = genericService.load(Menu.class, model.getId());
		List<Page> removedPages = getRemovedPages(oldMenu, model);
		if (removedPages.size() > 0) {
			// remove it if there's no reference
		}

		updateMenuPages(model);
		
		Menu oldParent = oldMenu.getParent();
		Menu newParent = model.getParent();
		boolean sameParent = newParent != null && oldParent != null && newParent.getId().equals(oldParent.getId())
				|| newParent == null && oldParent == null;
		Menu newMenu;
		if (sameParent) {
			newMenu = (Menu) genericService.update(model);
		} else {
			if (newParent == null) {
				model.setParent(null);
				genericService.update(model);
			} else {
				// update property
				model = genericService.update(model);
				
				// update relationship
				Menu newDbParent = genericService.load(Menu.class, newParent.getId());
				newDbParent.safeChildren().add(model);
				model.setParent(newDbParent);
				genericService.merge(newDbParent);
			}
			newMenu = model;
		}
		return newMenu;
	}
	
	
	private List<Page> getRemovedPages(Menu oldMenu, Menu newMenu) {
		List<Page> oldPages = new ArrayList<Page>();
		if (oldMenu.getPage() != null) {
			oldPages.add(oldMenu.getPage());
		}
		if (oldMenu.getRelevantPages() != null) {
			oldPages.addAll(oldMenu.getRelevantPages());
		}
		
		Collection<String> newPages = new HashSet<String>();
		if (newMenu.getPage() != null) {
			String path = newMenu.getPage().getPath();
			if (path != null) {
				newPages.add(path);
			}
		}
		if (newMenu.getRelevantPages() != null) {
			for (Page page : newMenu.getRelevantPages()) {
				if (page != null) {
					String path = page.getPath();
					if (path != null) {
						newPages.add(path);
					}
				}
			}
		}
		
		List<Page> removed = new ArrayList<Page>();
		for (Page page : oldPages) {
			String path = page.getPath();
			if (path != null && !newPages.contains(path)) {
				removed.add(page);
			}
		}
		return removed;
	}
	
	@Rest(type="search")
	public PageInfo searchResource(String path, String type, Integer pageNo, Integer pageSize, String query) {
		Invoker invoker = routing.path2Invoker(path, false);
		String modelName = beanIntrospector.getSimpleName(invoker.actionName);
		return resourceManagerService.searchResource(null, type, modelName, query, null, pageNo, pageSize);
	}
	
	@Transactional
	public boolean deleteList(List<Menu> results) {
		boolean ret = true;
		for (Menu menu : results) {
			if (menu != null) {
				boolean success = delete(menu);
				if (!success) {
					ret = false;
				}
			}
		}
		return ret;
	}
	
	@Transactional
	public boolean delete(Menu model) {		
		// 1. find the reference
		Menu dbMenu = genericService.load(model);
		
		Menu parent = dbMenu.getParent();
		if (parent != null) {
			parent.getChildren().remove(dbMenu);
			dbMenu.setParent(null);
			genericService.merge(parent);
		}
		
		dbMenu.setPage(null);
		dbMenu.setRelevantPages(null);
		genericService.merge(dbMenu);
		return genericService.delete(dbMenu);
	}
}